10.1 将应用部署到独立的服务器

让我们从最简单的部署方法开始——创建一个可执行的二进制文件,并将它放到互联网的某个服务器上运行,这个服务器可以是物理存在的,也可以是由Amazon Web Services(AWS)或者Digital Ocean等供应商创建的虚拟机(VM)。在本节中,我们将要学习如何在运行着Ubuntu Server 14.04系统的服务器上部署Go Web应用。

IaaS、PaaS和SaaS

云计算供应商都会通过不同的模型来为用户提供服务。美国国家标准技术研究所(National Institute of Standards and Technology, US Department of Commerce,NIST)定义了当今广为使用的3种服务模型,分别是基础设施即服务(Infrastructure-as-a-Service,IaaS),平台即服务(Platform-as-a- Service,PaaS)和软件即服务(Software-as-a-Service,SaaS)。 IaaS是这3种模型中最为基本的一种,使用这种模型的供应商将向他们的用户提供包括计算、存储以及网络在内的基本计算能力。提供IaaS服务的例子有AWS的弹性云计算服务(Elastic Cloud Computing Service,EC2)、Google公司的Compute Engine(计算引擎)以及Digital Ocean的Droplets。 使用PaaS模型的供应商会让用户通过他们提供的工具,将应用部署到云端的基础设施之上。提供PaaS服务的例子有Heroku、AWS的Elastic Beanstalk以及Google公司的App Engine。 使用SaaS模型的供应商会向用户提供应用服务。尽管消费者当今使用的绝大多数服务都可以看作是SaaS服务,但是在本书的语境中,我们只会把Heroku的Postgres 数据库服务(Postgres database service,它提供的是基于云的Postgres服务)、AWS的Relational Database Service(关系数据库服务,RDS)以及Google公司的Cloud SQL(云SQL)这样的服务看作是SaaS服务。 在本章中,我们将学习如何利用IaaS和PaaS供应商来部署GoWeb应用。

本书第7章介绍过的简单Web服务由代码清单10-1中的 data.go 和代码清单10-2中的 server.go 这两个文件组成,前者包含了所有指向数据库的连接和所有对数据库进行读写的函数,而后者则包含了 main 函数和Web服务的所有处理逻辑。

代码清单10-1 使用 data.go 访问数据库

package main
import (
 "database/sql"
 _ "github.com/lib/pq"
)
var Db *sql.DB
func init() {
 var err error
 Db, err = sql.Open("postgres", "user=gwp dbname=gwp password=gwp
 ➥sslmode=disable")
 if err != nil {
  panic(err)
 }
}
func retrieve(id int) (post Post, err error) {
 post = Post{}
 err = Db.QueryRow("select id, content, author from posts where id =
 ➥$1", id).Scan(&post.Id, &post.Content, &post.Author)
 return
}
func (post *Post) create() (err error) {
 statement := "insert into posts (content, author) values ($1, $2)
 ➥returning id"
 stmt, err := Db.Prepare(statement)
 if err != nil {
  return
 }
 defer stmt.Close()
 err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)
 return
}
func (post *Post) update() (err error) {
 _, err = Db.Exec("update posts set content = $2, author = $3 where id =
 ➥$1", post.Id, post.Content, post.Author)
 return
}
func (post *Post) delete() (err error) {
 _, err = Db.Exec("delete from posts where id = $1", post.Id)
 return
}

代码清单10-2 定义了Go Web服务的 server.go

package main
import (
 "encoding/json"
 "net/http"
 "path"
 "strconv"
)
type Post struct {
 Id   int  `json:"id"`
 Content string `json:"content"`
 Author string `json:"author"`
}
func main() {
 server := http.Server{
  Addr: "127.0.0.1:8080",
 }
 http.HandleFunc("/post/", handleRequest)
 server.ListenAndServe()
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
 var err error
 switch r.Method {
 case "GET":
  err = handleGet(w, r)
 case "POST":
  err = handlePost(w, r)
 case "PUT":
  err = handlePut(w, r)
 case "DELETE":
  err = handleDelete(w, r)
 }
 if err != nil {
  http.Error(w, err.Error(), http.StatusInternalServerError)
  return
 }
}
func handleGet(w http.ResponseWriter, r *http.Request) (err error) {
 id, err := strconv.Atoi(path.Base(r.URL.Path))
 if err != nil {
  return
 }
 post, err := retrieve(id)
 if err != nil {
  return
 }
 output, err := json.MarshalIndent(&post, "", "\t\t")
 if err != nil {
  return
 }
 w.Header().Set("Content-Type", "application/json")
 w.Write(output)
 return
}
func handlePost(w http.ResponseWriter, r *http.Request) (err error) {
 len := r.ContentLength
 body := make([]byte, len)
 r.Body.Read(body)
 var post Post
 json.Unmarshal(body, &post)
 err = post.create()
 if err != nil {
  return
 }
 w.WriteHeader(200)
 return
}
func handlePut(w http.ResponseWriter, r *http.Request) (err error) {
 id, err := strconv.Atoi(path.Base(r.URL.Path))
 if err != nil {
  return
 }
 post, err := retrieve(id)
 if err != nil {
  return
 }
 len := r.ContentLength
 body := make([]byte, len)
 r.Body.Read(body)
 json.Unmarshal(body, &post)
 err = post.update()
 if err != nil {
  return
 }
 w.WriteHeader(200)
 return
}
func handleDelete(w http.ResponseWriter, r *http.Request) (err error) {
 id, err := strconv.Atoi(path.Base(r.URL.Path))
 if err != nil {
  return
 }
 post, err := retrieve(id)
 if err != nil {
  return
 }
 err = post.delete()
 if err != nil {
  return
 }
 w.WriteHeader(200)
 return
}

首先,我们需要使用以下命令编译这段代码:

go build

如果我们把简单Web服务的代码放到一个名为 ws-s 的目录里,那么这个编译命令将产生一个同名的可执行二进制文件。为了部署Web服务 ws-s ,我们需要把ws-s文件复制到服务器里,并将其放置到一个可以通过外部访问的地方。

接着我们只需要登录服务器,并在终端里执行以下命令,就可以运行 ws-s 这个Web服务了:

./ws-s

需要注意的是,因为Web服务现在是在前台运行,所以在服务运行期间,我们将无法执行其他操作。与此同时,我们也无法简单地通过 & 命令或者 bg 命令在后台运行这个服务,因为这样做的话,一旦用户登出,Web服务就会被杀死。

避免上述问题的一种方法就是使用 nohup 命令,让操作系统在用户注销时,把发送至Web服务的 HUP (hangup,挂起)信号忽略掉:

nohup ./ws-s &

执行上述命令将导致Web服务被放到后台运行,并且不用担心因为 HUP 信号而被杀死。以这种方式启动的Web服务仍会如常地与客户端进行连接,但现在的Web服务将忽略所有挂起或者退出信号。因为这种状态下运行的Web服务在崩溃时将不会有任何提醒,所以在服务崩溃或者服务器重启之后,用户必须重新登入系统并重启服务。

nohup 之外,持续运行Web服务的另一种方法是使用Upstart或者systemd这样的 init 守护进程: init 进程是类Unix系统在启动时运行的第一个进程,该进程由内核负责启动,它会一直运行直到系统关闭为止,并且它还是其他所有进程直接或间接的祖先。

Upstart是由Ubuntu创建的一个基于事件的 init 替代品,尽管现在systemd也越来越受到大家的青睐,但考虑到这两个工具都能够完成本节介绍的工作,并且Upstart的使用方法相对来说要更为简单一些,所以我们接下来将要学习如何使用Upstart来持续地运行Web服务。

为了使用Upstart,用户首先需要创建一个对应的Upstart任务配置文件,并将该文件放到 etc/init 目录里面。对简单Web服务来说,我们将创建代码清单10-3所示的 ws.conf 文件,并将它放到 etc/init 目录里面。

代码清单10-3 简单Web服务的Upstart任务配置文件

respawn
respawn limit 10 5
setuid sausheong
setgid sausheong
exec /go/src/github.com/sausheong/ws-s/ws-s

这个Upstart任务配置文件非常简单和直接。文件中的每个Upstart任务都由一个或任意多个称为节(stanzas)的命令块组成。第一节 respawn 指示当任务失效(fail)时,Upstart将对其实施重新派生(respawn)或者重新启动。第二节 respawn limit 10 5respawn 设置了参数,它指示Upstart最多只会尝试重新派生该任务10次,并且每次尝试之间会有5 s的间隔;在用完了10次重新派生的机会之后,Upstart将不再尝试重新派生该任务,并将该任务视为已失效。第三节和第四节负责设置运行进程的用户以及用户组,而最后一节则是Upstart在启动任务时需要运行的可执行文件。

为了启动上述Upstart任务,我们需要在终端里面执行以下命令:

sudo start ws
ws start/running, process 2011

这个命令将触发Upstart读取 /etc/init/ws.conf 任务配置文件并启动任务。本节以管中窥豹的方式,快速地了解了如何使用简单的Upstart任务运行一个Go Web应用,但是除这里介绍的内容之外,Upstart的任务配置文件还有其他不同的节可供使用,并且Upstart的任务也拥有多种不同的配置方式可以使用,不过这些内容不在本书的介绍范围之内,有兴趣的读者可以自行通过互联网进行了解。

为了验证Upstart是否能够正确地运行和管理 ws-s 服务,我们可以尝试在Upstart任务启动之后,杀死正在运行的 ws-s 服务:

ps -ef | grep ws
sausheo+ 2011 1 0 17:23 ? 00:00:00 /go/src/github.com/sausheong/ws-s/ws-s
sudo kill -0 2011
ps -ef | grep ws
sausheo+ 2030 1 0 17:23 ? 00:00:00 /go/src/github.com/sausheong/ws-s/ws-s

注意看,在 kill 命令执行之前, ws-s 进程的ID为 2011 ,但是在 kill 命令执行之后, ws-s 进程的ID变成了 2030 ——这是因为Upstart在 kill 命令执行之后,察觉到了 ws-s 进程已被关闭,于是它重启了 ws-s 进程,从而导致 ws-s 进程的ID发生了变化。

最后,因为大部分Web应用都部署在标准HTTP端口(即80端口)之上,所以读者在实际部署时,应该将简单Web服务代码中的端口号从现在的8080改为80,或者通过某种机制将8080端口的流量代理或者重定向到80端口。

results matching ""

    No results matching ""